package com.sk.ccflower.server.lib.sockUtils.nio;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousCloseException;
import java.nio.channels.ClosedByInterruptException;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.NotYetConnectedException;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.util.Date;

import org.apache.log4j.Logger;

public class NioSession extends NioHandler
{
    protected SocketChannel m_sc = null;
    protected NioSelector m_selector = null;
    protected final Logger m_log = Logger.getLogger(this.getClass());

    protected final byte[] m_byte;
    protected final ByteBuffer m_buffer;
    protected final int m_bufferSize = 8192;

    public NioSession()
    {
        m_buffer = ByteBuffer.allocateDirect(m_bufferSize);
        m_byte = new byte[m_bufferSize];
    }

    public NioSession(SocketChannel sc)
    {
        this();
        m_sc = sc;
    }

    public NioSession(SocketChannel sc, NioSelector selector)
    {
        this(sc);
        m_selector = selector;
    }

    public void close()
    {
        if(m_sc==null) return;

        try {
            m_log.debug(m_sc.socket().getInetAddress().getHostAddress() + ":" + m_sc.socket().getPort() + " 클라이언트 접속 종료");
        } catch (Exception e) {
        }

        try {
            try {
                Thread.sleep(5);
            } catch (InterruptedException e) {
            	e.printStackTrace();
            }
            SocketChannel sc = m_sc;
            doCloseAction();
            if(sc==m_sc) m_sc = null;
            sc.close();
        } catch (Exception e) {
        	e.printStackTrace();
        }
    }

    public void cancel()
    {
        try {
            SelectionKey key = m_sc.keyFor(m_selector.getSelector());
            key.cancel();
        } catch (Exception e) {
        } finally {
            m_selector = null;
        }
    }

    protected void registerHandler() throws ClosedChannelException, Exception
    {
        m_selector.registerHandler(this, m_sc, SelectionKey.OP_READ);
    }

    public void registerHandler(NioSelector selector) throws ClosedChannelException, Exception
    {
        cancel();
        m_selector = selector;
        registerHandler();
    }

    public NioSelector getSelector()
    {
        return m_selector;
    }

    public SelectionKey getSelectionKey()
    {
        if(m_selector==null) return null;

        try {
            return m_sc.keyFor(m_selector.getSelector());
        } catch (Exception e) {
        }

        return null;
    }

    public SocketChannel getSockChannel()
    {
        return m_sc;
    }

    public int send(String s) throws Exception
    {
        byte[] b = s.getBytes();
        return send(b, 0, s.length());
    }

    public int send(byte[] b) throws Exception
    {
        return send(b, 0, b.length);
    }

    public int send(byte[] b, int offset, int length) throws Exception
    {
        if(doSendAction(b, offset, length)==false) return length;
        return sendAbsolute(b, offset, length);
    }

    public int sendAbsolute(byte[] b, int offset, int length) throws Exception
    {
        int writeLen = 0;
        int writeTotalLen = 0;
        int retryCount = 0;
        //this.m_log.debug("Send Data to "+m_sc.socket().getInetAddress().getHostAddress()+" [" + new String(b) + "] -> " + length);
        //for( int i = 0 ; i < b.length ; i++){
            //this.m_log.debug(i +"="+ b[i]);
        //}
        while(length>0)
        {
            m_buffer.clear();
            writeLen = length < m_buffer.limit() ? length : m_buffer.limit();
            m_buffer.put(b, offset, writeLen);
            m_buffer.flip();

            try
            {
                writeLen = m_sc.write(m_buffer);

                if(writeLen==0)
                {
                    if(retryCount>10)
                    {
                        close();
                        throw new Exception("send: 데이터 전송중 접속 세션에서 응답 없음");
                    }

                    retryCount ++;
                    Thread.sleep(500);
                }
                else if(writeLen<0)
                {
                    throw new Exception("데이터 전송중 접속 끊김");
                }

                offset += writeLen;
                length -= writeLen;
                writeTotalLen += writeLen;

            }
            catch ( NotYetConnectedException e1 ) {
            	e1.printStackTrace();
            	m_log.error( "e1: "+e1.getMessage(), e1 );
            	throw new Exception( "send error: " + e1.getMessage(), e1 );
            }
            catch ( ClosedByInterruptException e2 ) {
            	e2.printStackTrace();
            	m_log.error( "e2: "+e2.getMessage(), e2 );
            	throw new Exception( "send error: " + e2.getMessage(), e2 );
            }
            catch ( AsynchronousCloseException e3 ) {
            	e3.printStackTrace();
            	m_log.error( "e3: "+e3.getMessage(), e3 );
            	throw new Exception( "send error: " + e3.getMessage(), e3 );
            }
            catch ( ClosedChannelException e4 ) {
            	e4.printStackTrace();
            	m_log.error( "e4: "+e4.getMessage(), e4 );
            	throw new Exception( "send error: " + e4.getMessage(), e4 );
            }
            catch ( IOException e5 ) {
            	e5.printStackTrace();
            	m_log.error( "e5: "+e5.getMessage(), e5 );
                close();
                throw new Exception( "send error: " + e5.getMessage(), e5 );
            }
        }

        return length;
    }

    public int recv(ByteBuffer buffer) throws Exception
    {
        if(buffer==null) throw new Exception("ByteBuffer is null");

        int readLen = 0;

        try {
            readLen = m_sc.read(buffer);

            if(readLen<0) {
                close();
            } else if(readLen>0) {
                if(buffer.hasArray()) {
                    doRecvAction(buffer.array(), buffer.position()-readLen, readLen);
                } else {
                    buffer.flip();
                    int leftLen = readLen;

                    while(true) {
                        if(leftLen>m_byte.length) {
                            buffer.get(m_byte, 0, m_byte.length);
                            doRecvAction(m_byte, 0, m_byte.length);
                            leftLen -= m_byte.length;
                        } else {
                            buffer.get(m_byte, 0, leftLen);
                            doRecvAction(m_byte, 0, leftLen);
                            break;
                        }
                    }
                }
            }
        } catch (IOException e) {
            close();
        }

        return readLen;
    }

    public int recv(byte[] b) throws Exception
    {
        return recv(b, 0, b.length);
    }

    public int recv(byte[] b, int offset, int length) throws Exception
    {
        if(b==null) throw new Exception("byte is null");

        ByteBuffer buffer = ByteBuffer.wrap(b, offset, length);
        return recv(buffer);
    }

    private int recv() throws Exception
    {
        m_buffer.clear();
        return recv(m_buffer);
    }

    @Override
    protected void handleInput(SelectionKey key)
    {
        if(!key.isReadable()) {
            m_log.error("key is not readable !!");
            return;
        }

        try {
            recv();
        } catch (Exception e) {
        }
    }

    @Override
    protected void handleTimeout(SelectionKey key, Date date)
    {
        doTimeoutAction(date);
    }

    /**
     * 처음 접속이 이루워진 후 호출된다. 하위 클래스에서 재 정의하여 사용한다.
     * @throws Exception: exception을 발생시키면 connect()에서 해당 exception을 발생하고 접속을 끊는다.
     */
    protected void doConnectAction() throws Exception
    {
    }

    /**
     * 접속이 종료 후 호출 된다. 하위 클래스에서 재 정의하여 사용한다.
     */
    protected void doCloseAction()
    {
    }

    /**
     * 데이터를 수신하였을 때 호출 된다. 하위 클래스에서 재 정의하여 사용한다.
     */
    protected void doRecvAction(byte[] b, int offset, int length)
    {
    }

    /**
     * 데이터를 전송하기 전에 호출된다. 하위 클래스에서 재 정의하여 사용한다.
     * @return true:데이터를 전송함/false:데이터를 전송하지 않음
     */
    protected boolean doSendAction(byte[] b, int offset, int length)
    {
        return true;
    }

    /**
     * 등록한 Timeout에 의해 timeout이 발생하면 호출됨, 하위 클래스에서 재 정의하여 사용한다.
     */
    protected void doTimeoutAction(Date date)
    {
    }
}
